﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading;
using Microsoft.TeamFoundation.Client;
using Microsoft.TeamFoundation.VersionControl.Client;
using System.Linq;
using NLog;

namespace TeamCityConfigurationMonitor.Core
{
    class Synchronizer : IDisposable
    {
        private readonly IConfiguration configuration;
        private readonly IChangeProvider changeProvider;
        private readonly IChangeNotifier changeNotifier;

        private Workspace workspace;
        private TfsTeamProjectCollection teamProjectCollection;
        private VersionControlServer vcs;

        private readonly object synchronizationLock = new object();
        private readonly Logger logger = LogManager.GetCurrentClassLogger();

        public Synchronizer(IConfiguration configuration, IChangeProvider changeProvider, IChangeNotifier changeNotifier)
        {
            this.configuration = configuration;
            this.changeProvider = changeProvider;
            this.changeNotifier = changeNotifier;

            Bootstrap();
        }

        private void Bootstrap()
        {
            teamProjectCollection = CreateTeamProjectCollection();

            vcs = teamProjectCollection.GetService<VersionControlServer>();

            if (!IsServerPathValid())
                throw new InvalidOperationException("Server path setting must point to a valid folder on the server");

            workspace = WorkspaceExists() ? GetWorkspace() : CreateWorkspace();

            if (ThereArePendingChanges())
                UndoPendingChanges();

            CleanupWorkspace();

            ForceGetLatestVersion();
        }

        private TfsTeamProjectCollection CreateTeamProjectCollection()
        {
            var c = configuration.HasCredentials
                                        ? new TfsTeamProjectCollection(configuration.TfsUri, configuration.Credentials)
                                        : new TfsTeamProjectCollection(configuration.TfsUri);

            c.Authenticate();

            return c;
        }

        private bool IsServerPathValid()
        {
            var item = vcs.GetItem(configuration.ServerPath);

            return item != null && item.ItemType == ItemType.Folder;
        }

        private void CleanupWorkspace()
        {
            logger.Info("Trying to delete local workspace directory");
            FileSystem.DeleteDirectory(configuration.WorkspacePath);

            logger.Info("Creating local workspace directory");
            Directory.CreateDirectory(configuration.WorkspacePath);
        }

        public void Synchronize()
        {
            logger.Info("Starting synchronization");

            if (!Monitor.TryEnter(synchronizationLock))
            {
                logger.Debug("Cannot start synchronization as it is currently being performed");
                changeNotifier.NotifyLater();
                return;
            }

            try
            {
                var configurationFolder = configuration.ConfigurationLocation;
                var workspaceFolder = configuration.WorkspacePath;

                var localChanges = changeProvider.GetAndResetChanges();
                var configurationFiles = GetFilesSpec(configurationFolder, workspaceFolder);
                var workspaceFiles = GetFilesSpec(workspaceFolder, configurationFolder);
                var workspaceDirectories = GetDirectoriesSpec(workspaceFolder, configurationFolder);

                var filesToDelete = workspaceFiles.Where(spec => !File.Exists(spec.TargetPath)).ToArray();
                var directoriesToDelete = workspaceDirectories.Where(spec => !Directory.Exists(spec.TargetPath)).ToArray();
                var filesToAdd = configurationFiles.Where(spec => !File.Exists(spec.TargetPath)).ToArray();
                var filesToOverwrite = configurationFiles.Except(filesToAdd).ToArray();

                PerformFileDeletions(filesToDelete);
                PerformDirectoryDeletions(directoriesToDelete);

                PerformAdditions(filesToAdd);

                PerformOverwrites(filesToOverwrite);

                logger.Info("Checking-in pending changes");
                
                var changeset = workspace.CheckIn(workspace.GetPendingChanges(), BuildCheckinComment(localChanges));

                LogCheckinResult(changeset);
            }
            finally
            {
                logger.Info("Completed synchronization");
                Monitor.Exit(synchronizationLock);
            }
        }

        private void LogCheckinResult(int changeset)
        {
            var result = new StringBuilder("Checkin result:");

            if (changeset != 0)
            {
                var changes = (from change in vcs.GetChangeset(changeset).Changes
                               group change by change.ChangeType
                               into g
                               select new {ChangeType = g.Key, Count = g.Count()}).ToArray();

                foreach (var c in changes)
                    result.AppendFormat(" {0}: {1},", c.ChangeType, c.Count);

                result.Remove(result.Length - 1, 1);
            }
            else
            {
                result.Append(" no changes");
            }

            logger.Info(result.ToString);
        }

        private void PerformOverwrites(IEnumerable<FileSpec> filesToOverwrite)
        {
            if(filesToOverwrite.Any())
            {
                logger.Debug("Overwriting {0} files (eventually)", filesToOverwrite.Count());
                workspace.PendEdit(filesToOverwrite.Select(f => f.TargetPath).ToArray());
            }

            foreach (var spec in filesToOverwrite)
                File.Copy(spec.Path, spec.TargetPath, true);
        }

        private void PerformAdditions(IEnumerable<FileSpec> filesToAdd)
        {
            foreach (var spec in filesToAdd)
            {
                var targetFolder = Path.GetDirectoryName(spec.TargetPath);

                if (!Directory.Exists(targetFolder))
                    Directory.CreateDirectory(targetFolder);

                File.Copy(spec.Path, spec.TargetPath);
            }

            if(filesToAdd.Any())
            {
                logger.Debug("Adding {0} files", filesToAdd.Count());
                workspace.PendAdd(filesToAdd.Select(f => f.TargetPath).ToArray());
            }
        }

        private void PerformFileDeletions(IEnumerable<FileSpec> filesToDelete)
        {
            if (filesToDelete.Any())
            {
                logger.Debug("Deleting {0} files", filesToDelete.Count());
                workspace.PendDelete(filesToDelete.Select(f => f.Path).ToArray());
            }
        }

        private void PerformDirectoryDeletions(IEnumerable<FileSpec> directoriesToDelete)
        {
            if (directoriesToDelete.Any())
            {
                logger.Debug("Deleting {0} directories", directoriesToDelete.Count());
                workspace.PendDelete(directoriesToDelete.Select(f => f.Path).ToArray());
            }
        }

        private static string BuildCheckinComment(IEnumerable<FileSystemChange> fileSystemChanges)
        {
            var grouped = fileSystemChanges.GroupBy(change => change.ChangeType)
                .Select(g => new { ChangeType = g.Key.ToString(), NumberOfChanges = g.Count() });

            var result = new StringBuilder("TeamCity configuration monitor synchronization.");

            if (grouped.Any())
                result.AppendLine();

            foreach (var g in grouped)
                result.AppendLine().AppendFormat("{0}: {1}", g.ChangeType, g.NumberOfChanges);

            return result.ToString();
        }

        private IEnumerable<FileSpec> GetFilesSpec(string sourceFolder, string targetFolder)
        {
            return Directory.GetFiles(sourceFolder, configuration.FilesToMonitor, SearchOption.AllDirectories)
                .Select(f => new
                             {
                                 Path = f,
                                 RelativePath = f.Substring(sourceFolder.Length).TrimStart(Path.DirectorySeparatorChar)
                             })
                .Select(f => new FileSpec
                             {
                                 Path = f.Path,
                                 TargetPath = Path.Combine(targetFolder, f.RelativePath)
                             })
                // important! greedy
                .ToArray();
        }

        private static IEnumerable<FileSpec> GetDirectoriesSpec(string sourceFolder, string targetFolder)
        {
            return Directory.GetDirectories(sourceFolder, "*.*", SearchOption.AllDirectories)
               .Select(f => new
               {
                   Path = f,
                   RelativePath = f.Substring(sourceFolder.Length).TrimStart(Path.DirectorySeparatorChar)
               })
               .Select(f => new FileSpec
               {
                   Path = f.Path,
                   TargetPath = Path.Combine(targetFolder, f.RelativePath)
               })
                // important! greedy
               .ToArray();
        }

        private void ForceGetLatestVersion()
        {
            logger.Info("Force get latest version");
            workspace.Get(VersionSpec.Latest, GetOptions.GetAll | GetOptions.Overwrite);
        }

        private void UndoPendingChanges()
        {
            logger.Info("Undoing local pending changes");
            workspace.Undo(workspace.GetPendingChanges().Select(p => p.LocalItem).ToArray());
        }

        private Workspace GetWorkspace()
        {
            logger.Debug("Querying local workspace");
            return GetLocalWorkspaceInfo().GetWorkspace(vcs.TeamProjectCollection);
        }

        private bool ThereArePendingChanges()
        {
            var thereArePendingChanges = workspace.GetPendingChangesEnumerable().Any();

            if(thereArePendingChanges)
                logger.Info("Found local pending changes");

            return thereArePendingChanges;
        }

        private Workspace CreateWorkspace()
        {
            logger.Info("Creating local workspace {0} in {1}", configuration.WorkspaceName, configuration.WorkspacePath);
            return vcs.CreateWorkspace(configuration.WorkspaceName, vcs.AuthorizedUser, string.Format("Workspace created by {0}", configuration.WorkspaceName),
                                       new[] { new WorkingFolder(configuration.ServerPath, configuration.WorkspacePath) });
        }

        private bool WorkspaceExists()
        {
            return GetLocalWorkspaceInfo() != null;
        }

        private WorkspaceInfo GetLocalWorkspaceInfo()
        {
            return Workstation.Current.GetLocalWorkspaceInfo(vcs, configuration.WorkspaceName, vcs.AuthorizedUser);
        }

        public void Dispose()
        {
            teamProjectCollection.Dispose();
        }
    }
}